﻿namespace GreenIsomer.Web.Controls.JQuery {
	using System;
	using System.Text;
	using System.Web.UI;
	using System.Globalization;

	/// <summary>
	/// Provides a structured way to generate JavaScript/jQuery.
	/// </summary>
	public class ScriptBuilder {


		#region Constants

		private static readonly char[] encodedChars = { '\\', '\'', '\r', '\n', '\t' }; // TODO: Add more (especially control characters)

		#endregion


		#region Static Methods

		/// <summary>
		/// Escapes the quotes for a JavaScript literal.
		/// </summary>
		/// <param name="value">The value to encode/escape.</param>
		/// <returns>An encoded literal that can be used directly in generated JavaScript.</returns>
		public static string JavaScriptStringEncode(string value) {
			if(string.IsNullOrEmpty(value)) return string.Empty;

			// do not create new string if not needed
			if(value.IndexOfAny(encodedChars) < 0)
				return value;
			
			StringBuilder result = new StringBuilder(value.Length + 8); // assume string will only expand a bit in most cases
			for(int i = 0; i < value.Length; i++) {
				char c = value[i];

				// replace special characters
				switch(c) {
					case '\\': result.Append(@"\\"); break;
					case '\'': result.Append(@"\'"); break;
					case '\r':
						if(i == (value.Length - 1) || value[i + 1] != '\n') result.Append(@"\n");
						break;
					case '\n': result.Append(@"\n"); break;
					case '\t': result.Append(@"\t"); break;
					default: result.Append(c); break;
				}
			}
			return result.ToString();
		}

		/// <summary>
		/// Generates a JavaScript contructor for a Date/Time value.
		/// </summary>
		/// <param name="value">The System.DateTime value to create in JavaScript.</param>
		/// <param name="includeTime">Whether or not to include the time.</param>
		/// <returns>A JavaScript Date/Time constructor.</returns>
		public static string GetCreateDateTimeScript(DateTime value, bool includeTime) {
			//TODO: Implement
			if(includeTime) throw new NotImplementedException();

			//Javascript month is zero based
			return string.Format(CultureInfo.InvariantCulture, "new Date({0},{1},{2})", value.Year, value.Month - 1, value.Day);
		}

		/// <summary>
		/// Coverts a Pascal-cased standard .NET Enum value and converts it to an encoded camel-case JavaScript literal.
		/// </summary>
		/// <typeparam name="T">An Enum member.</typeparam>
		/// <param name="value">The Enum value to encode.</param>
		/// <returns>A camel-case JavaScript string literal.</returns>
		/// <exception cref="ArgumentException">Value is not defined.</exception>
		public static string GetCamelCaseEnumValue<T>(T value) where T : struct {
			if(!Enum.IsDefined(typeof(T), value)) throw new ArgumentException("Value is not defined.", "value");

			StringBuilder result = new StringBuilder(value.ToString());
			result[0] = char.ToLowerInvariant(result[0]);

			return result.ToString();
		}

		#endregion


		#region Members 

		private StringBuilder script;

		#endregion


		#region Constructors

		/// <summary>
		/// Initializes a new instance of the GreenIsomer.Web.Controls.JQuery.ScriptBuilder class.
		/// </summary>
		public ScriptBuilder() {
			script = new StringBuilder();
		}

		#endregion


		#region Properties: Public

		/// <summary>
		/// Gets or sets the current indentation level of the script being generated.
		/// </summary>
		public virtual int Indent { get; set; }

		/// <summary>
		/// Gets the length of the currently appended script.
		/// </summary>
		public virtual int Length {
			get { return Script.Length; }
		}

		#endregion


		#region Properties: State

		/// <summary>
		/// Gets or sets the wrapped System.Text.StringBuilder used by the script builder.
		/// </summary>
		protected virtual StringBuilder Script {
			get { return script; }
			set { script = value; }
		}

		/// <summary>
		/// Gets or sets whether or not the last line of script appended ha ended.
		/// </summary>
		protected virtual bool LineEnded { get; set; }

		/// <summary>
		/// Gets or sets the maximum number of lines the user of the script has instructed to skip since the last line was written.
		/// </summary>
		protected virtual int PendingLineSkips { get; set; }

		/// <summary>
		/// Gets or sets whether a new line has been started since the previous line ended.
		/// </summary>
		protected virtual bool LineStarted { get; set; }

		#endregion


		#region Methods: State

		/// <summary>
		/// Instruct the script builder to write any text that was being withheld pending a new line of code.
		/// </summary>
		protected virtual void AppendPending() {
			if(LineStarted) return;

			if(LineEnded) {
				Script.AppendLine();
				LineEnded = false;
			}

			for(int i = 0; i < PendingLineSkips; i++)
				Script.AppendLine();
			PendingLineSkips = 0;

			Script.Append(new string('\t', Indent));

			LineStarted = true;
		}

		#endregion


		#region Methods: Script

		/// <summary>
		/// Adds literal script to the end of the currently generated script.
		/// </summary>
		public virtual void Append(string value) {
			AppendPending();
			Script.Append(value);
		}

		/// <summary>
		/// Adds a JavaScript literal boolean to the script.
		/// </summary>
		/// <param name="value">A boolean value.</param>
		public virtual void AppendValue(bool value) {
			Append(value ? "true" : "false");
		}

		/// <summary>
		/// Adds a JavaScript literal string to the script.
		/// </summary>
		/// <param name="value">A character value.</param>
		public virtual void AppendValue(char value) {
			AppendValue(value.ToString(CultureInfo.InvariantCulture));
		}

		/// <summary>
		/// Adds a JavaScript literal string to the script.
		/// </summary>
		/// <param name="buffer">A character array.</param>
		public virtual void AppendValue(char[] buffer) {
			AppendValue(buffer ==  null ? null : new string(buffer));
		}

		/// <summary>
		/// Adds a JavaScript literal string to the script.
		/// </summary>
		/// <param name="buffer">A character array.</param>
		/// <param name="index">The first character to append from the array.</param>
		/// <param name="count">The number of characters to append.</param>
		public virtual void AppendValue(char[] buffer, int index, int count) {
			AppendValue(buffer == null ? null : new string(buffer, index, count));
		}

		/// <summary>
		/// Adds a literal JavaScript Date/Time string to the script.
		/// </summary>
		/// <param name="value">A DateTime value.</param>
		/// <param name="includeTime">Whether or not to include the time portion of the DateTime.</param>
		public virtual void AppendValue(DateTime value, bool includeTime) {
			Append(GetCreateDateTimeScript(value, includeTime));
		}

		/// <summary>
		/// Adds a literal numeric value to the script.
		/// </summary>
		/// <param name="value">A numeric value.</param>
		public virtual void AppendValue(decimal value) {
			Append(value.ToString(CultureInfo.InvariantCulture));
		}

		/// <summary>
		/// Adds a literal numeric value to the script.
		/// </summary>
		/// <param name="value">A numeric value.</param>
		public virtual void AppendValue(double value) {
			Append(value.ToString(CultureInfo.InvariantCulture));
		}

		/// <summary>
		/// Adds a literal numeric value to the script.
		/// </summary>
		/// <param name="value">A numeric value.</param>
		public virtual void AppendValue(float value) {
			Append(value.ToString(CultureInfo.InvariantCulture));
		}

		/// <summary>
		/// Adds a literal numeric value to the script.
		/// </summary>
		/// <param name="value">A numeric value.</param>
		public virtual void AppendValue(int value) {
			Append(value.ToString(CultureInfo.InvariantCulture));
		}

		/// <summary>
		/// Adds a literal numeric value to the script.
		/// </summary>
		/// <param name="value">A numeric value.</param>
		public virtual void AppendValue(long value) {
			Append(value.ToString(CultureInfo.InvariantCulture));
		}

		/// <summary>
		/// Adds a literal string to the script using an object's default text output.
		/// </summary>
		/// <param name="value">A value from which to extract a string literal.</param>
		public virtual void AppendValue(object value) {
			if(value == null) Append("null");
			else AppendValue(value.ToString());
		}

		/// <summary>
		/// Adds a literal string to the script.
		/// </summary>
		/// <param name="value">A string value.</param>
		public virtual void AppendValue(string value) {
			if(value == null) Append("null");
			else Append("'" + JavaScriptStringEncode(value) + "'");
		}

		/// <summary>
		/// Adds a literal numeric value to the script.
		/// </summary>
		/// <param name="value">A numeric value.</param>
		[CLSCompliant(false)]
		public virtual void AppendValue(uint value) {
			Append(value.ToString(CultureInfo.InvariantCulture));
		}

		/// <summary>
		/// Adds a literal numeric value to the script.
		/// </summary>
		/// <param name="value">A numeric value.</param>
		[CLSCompliant(false)]
		public virtual void AppendValue(ulong value) {
			Append(value.ToString(CultureInfo.InvariantCulture));
		}

		/// <summary>
		/// Adds a literal string value to the script, replacing the format item in a specified System.String with the text equivalent of the value of a corresponding object instance in a specified array.
		/// </summary>
		/// <param name="provider">A System.IFormatProvider that supplies culture-specific formatting information.</param>
		/// <param name="format">A composite format string.</param>
		/// <param name="args">An object array containing zero or more objects to format.</param>
		public virtual void AppendFormat(IFormatProvider provider, string format, params object[] args) {
			AppendValue(string.Format(provider, format, args));
		}

		/// <summary>
		/// Converts a Pascal-cased enumeration value to a literal JavaScript camel-cased string.
		/// </summary>
		/// <typeparam name="T">The enumeration type to convert.</typeparam>
		/// <param name="value">The value to convert.</param>
		public virtual void AppendEnumValue<T>(T value) where T : struct {
			AppendValue(GetCamelCaseEnumValue(value));
		}

		/// <summary>
		/// Appends a jQuery selector string to select an element by its client ID.
		/// </summary>
		/// <param name="clientId">The target control's client identifier.</param>
		public virtual void RenderSelector(string clientId) {
			RenderSelector(clientId, string.Empty);
		}

		/// <summary>
		/// Appends a jQuery selector string to select an element by its client ID.
		/// </summary>
		/// <param name="clientId">The target control's client identifier.</param>
		/// <param name="selectorSuffix">Additional jQuery selector options, such as ":input".</param>
		public virtual void RenderSelector(string clientId, string selectorSuffix) {
			if(clientId == null) throw new ArgumentNullException("clientId");
			if(clientId.Length == 0) throw new ArgumentException("A value is required.", "clientId");

			if(selectorSuffix == null) selectorSuffix = string.Empty;

			AppendPending();

			Script.Append("$(");
			AppendValue("#" + clientId + selectorSuffix); //TODO: Escaping for selector is a little more complicated, special characters are escaped with \
			Script.Append(")");
		}

		/// <summary>
		/// Appends jQuery initialization options based on a list of properties created by a jQuery server control.
		/// </summary>
		/// <param name="properties">The properties to render.</param>
		public virtual void RenderProperties(PropertyCollection properties) {

			if(properties.Count == 0) return;

			AppendPending();

			Script.Append("{ ");

			bool firstProperty = true;
			foreach(string property in properties) {
				if(firstProperty) firstProperty = false;
				else Script.Append(", ");

				Script.Append(property);
			}

			Script.Append(" }");
		}

		/// <summary>
		/// Instructs the script builder to start a new line before appending any more script or values.
		/// </summary>
		public virtual void NewLine() {
			if(Script.Length == 0) return;
			LineEnded = true;
			LineStarted = false;
		}

		/// <summary>
		/// Instructs the script builder to start a new line if necessary and skip a single line before appending any more script or values.
		/// </summary>
		public virtual void SkipLine() {
			SkipLines(1);
		}

		/// <summary>
		/// Instructs the script builder to start a new line if necessary and skip a specified number of lines befor appending any more script or values.
		/// </summary>
		/// <param name="count">The number of lines to skip.</param>
		public virtual void SkipLines(int count) {
			if(Script.Length == 0) return;
			LineEnded = true;
			if(PendingLineSkips < count) PendingLineSkips = count;
			LineStarted = false;
		}

		/// <summary>
		/// Returns the final script appended to the script builder.
		/// </summary>
		/// <returns>The final generated JavaScript code.</returns>
		public override string ToString() {
			return Script.ToString();
		}

		#endregion


	}

}